// Fill out your copyright notice in the Description page of Project Settings.

// https://ue4community.wiki/legacy/savegame-pointers-and-structs-8wlg0qms
// https://ue4community.wiki/legacy/savegame-pointers-and-structs-8wlg0qms
#include "FrameWork/BaseSaveGame.h"
#include "GameFramework/Actor.h"
#include "Serialization/MemoryReader.h"
#include "Serialization/MemoryWriter.h"
#include "Engine/World.h"
#include "Kismet/GameplayStatics.h"
#include "Misc/FileHelper.h"
#include "Serialization/ArchiveLoadCompressedProxy.h"
#include "Serialization/ArchiveSaveCompressedProxy.h"

DEFINE_LOG_CATEGORY(LogSaveGame)

void UBaseSaveGame::ActorArraySaver(UPARAM(ref) TArray<AActor*>& SaveActors)
{
	for (AActor* SaveActor : SaveActors)
	{
		ActorSaver(SaveActor);
	}
}

void UBaseSaveGame::ActorSaver(AActor* SaveActor)
{
	int32 Index = ObjectRecords.Emplace();
	FObjectRecord& ObjectRecord = ObjectRecords[Index];

	ObjectRecord.Name = SaveActor->GetFName();
	ObjectRecord.Transform = SaveActor->GetTransform();
	ObjectRecord.Class = SaveActor->GetClass();
	ObjectRecord.bActor = true;

	SaveData(SaveActor, ObjectRecord.Data);

	this->TempObjects.Add(SaveActor);
	// UE_LOG(LogSaveGame, Display, TEXT("Complete Save Actor %s"), *SaveActor->GetName())
}

void UBaseSaveGame::ActorPreloader(AActor* WorldActor, FObjectRecord& ActorRecord)
{
	FActorSpawnParameters SpawnParams;
	SpawnParams.Name = ActorRecord.Name;

	// TODO: change this to SpawnActorDeferred so you can de-serialize and apply data before it calls constructor\BeginPlay
	AActor* NewActor = WorldActor->GetWorld()->SpawnActor(ActorRecord.Class, &ActorRecord.Transform, SpawnParams);
	// AActor* NewActor = WorldActor->GetWorld()->SpawnActorDeferred

	// BUG? actor doesn't appear to load scale correctly using transform so I specifically apply the scale after loading
	NewActor->SetActorScale3D(ActorRecord.Transform.GetScale3D());

	// don't load now, load after all objects are preloaded
	// LoadData(LoadObject, ObjectRecord.Data);

	// add to temp array for lookup it another object using already loaded objects as outers (array gets cleared once all objects loaded)
	this->TempObjects.Add(NewActor);

	// UE_LOG(LogSaveGame, Display, TEXT("Complete Load Actor %s"), *NewActor->GetPathName())
}

void UBaseSaveGame::UObjectArraySaver(UPARAM(ref) TArray<UObject*>& SaveObjects)
{
	for (UObject* SaveObject : SaveObjects)
	{
		UObjectSaver(SaveObject);
	}
}

void UBaseSaveGame::UObjectSaver(UObject* SaveObject)
{
	if (SaveObject == nullptr)
	{
		// UE_LOG(LogSaveGame, Error, TEXT("Invalid Save Object!"))
		return;
	}

	if (SaveObject->HasAnyFlags(EObjectFlags::RF_Transient))
	{
		// UE_LOG(LogSaveGame, Warning, TEXT("Saving RF_Transient object"))
		return;
	}

	if (SaveObject->IsA(SaveObject->StaticClass()))
	{
		ActorSaver(Cast<AActor>(SaveObject));
		return;
	}

	const int32 Index = ObjectRecords.Emplace();
	FObjectRecord& ObjectRecord = ObjectRecords[Index];

	// Use custom IDs for save\retrieving outer pointers
	// * Negative IDs if outer is a permanent map object (i.e. not loaded from SaveGame)
	// * Negative IDs start from -2 because -1 is already assigned to INDEX_NONE, and 0+ is used for SaveGame loaded objects
	ObjectRecord.OuterID = TempObjects.Find(SaveObject->GetOuter());
	ObjectRecord.bActor = false;

	// if outer is a saved object then don't try to save the direct object pointer
	if (ObjectRecord.OuterID == INDEX_NONE)
	{
		ObjectRecord.OuterID = PersistentOuters.Find(SaveObject->GetOuter());
		if (ObjectRecord.OuterID != INDEX_NONE)
		{
			ObjectRecord.OuterID = -(ObjectRecord.OuterID + 2);
		} else
		{
			const int32 index = PersistentOuters.Add(SaveObject->GetOuter());
			ObjectRecord.OuterID = -(index + 2);
			// UE_LOG(LogSaveGame, Display, TEXT("Save Outer %s"), *SaveObject->GetOuter()->GetPathName())
		}
	}

	ObjectRecord.Name = SaveObject->GetFName();
	ObjectRecord.Class = SaveObject->GetClass();

	SaveData(SaveObject, ObjectRecord.Data);

	this->TempObjects.Add(SaveObject);

	// UE_LOG(LogSaveGame, Display, TEXT("Complete Save UObject %s"), *SaveObject->GetName())
}

void UBaseSaveGame::UObjectsPreloader(AActor* WorldActor)
{
	UObject* LoadOuter = nullptr;

	for (FObjectRecord& ObjectRecord : ObjectRecords)
	{
		if (ObjectRecord.bActor == false)
		{
			if (ObjectRecord.OuterID != INDEX_NONE)
			{
				if (TempObjects.IsValidIndex(ObjectRecord.OuterID) == true)
				{
					LoadOuter = TempObjects[ObjectRecord.OuterID];
					if (LoadOuter == nullptr)
					{
						// UE_LOG(LogSaveGame, Error, TEXT("Unable to find Outer for object (invalid array object)"))
					}
				} else
				{
					int32 NewIndex = FMath::Abs(ObjectRecord.OuterID) - 2;

					if (PersistentOuters.IsValidIndex(NewIndex))
					{
						LoadOuter = PersistentOuters[NewIndex];
					} else
					{
						// UE_LOG(LogSaveGame, Error, TEXT("Unable to find Outer for object (invalid ID)"))
					}
				}
			}
			if (LoadOuter == nullptr)
			{
				// UE_LOG(LogSaveGame, Error, TEXT("Unable to find Outer for object (no pointer)"))
				continue;
			}

			UObject* LoadObject = NewObject<UObject>(LoadOuter, ObjectRecord.Class, ObjectRecord.Name);

			if (LoadObject == nullptr) return;

			// don't load now, load after all objects are preloaded
			//LoadData(LoadObject, ObjectRecord.Data);

			// add to here to cycle through and keep a pointer temporarly to avoid garbage collection (not sure if required but to be safe)
			this->TempObjects.Add(LoadObject);

			// UE_LOG(LogSaveGame, Display, TEXT("Complete Load UObject %s %d"), *LoadObject->GetPathName(), this->TempObjects.Num() - 1)
		} else
		{
			ActorPreloader(WorldActor, ObjectRecord);
		}
	}
}

void UBaseSaveGame::UObjectDataLoader()
{
	for (int32 a = 0; ObjectRecords.IsValidIndex(a); a++)
	{
		// Load now after all objects are preloaded
		LoadData(TempObjects[a], ObjectRecords[a].Data);
	}
}

void UBaseSaveGame::SaveData(UObject* Object, TArray<uint8>& Data)
{
	if (Object == nullptr) return;

	FMemoryWriter MemoryWriter = FMemoryWriter(Data, true);
	FWCSaveGameArchive MyArchive(MemoryWriter);

	Object->Serialize(MyArchive);
}

void UBaseSaveGame::LoadData(UObject* Object, UPARAM(ref) TArray<uint8>& Data)
{
	if (Object == nullptr) return;

	FMemoryReader MemoryReader(Data, true);

	FWCSaveGameArchive Ar(MemoryReader);
	Object->Serialize(Ar);
}

void UBaseSaveGame::SaveLoadData(FArchive& Ar, int32& SaveDataInt32, FVector& SaveDataVector, TArray<FRotator>& SaveDataRotatorArray)
{
	Ar << SaveDataInt32;
	Ar << SaveDataVector;
	Ar << SaveDataRotatorArray;
}

bool UBaseSaveGame::SaveGameDataToFile(const FString& FullFilePath)
{
	// note that the supplied FString must be the entire Filepath
	// 	if writing it out yourself in C++ make sure to use the \\
	// 	for example:

	// 	FString SavePath = "C:\\MyProject\\MySaveDir\\mysavefile.save";

	// Step 1: Variable Data -> Binary

	// following along from above examples
	FBufferArchive ToBinary;
	// FMemoryWriter ToBinary();
	SaveLoadData(ToBinary, NumGemsCollected, PlayerLocation, ArrayOfRotationsOfTheStars);
	// presumed to be global var data, 
	// could pass in the data too if you preferred

	//No Data
	if (ToBinary.Num() <= 0) return false;
	//~

	//Step 2: Binary to Hard Disk
	if (FFileHelper::SaveArrayToFile(ToBinary, *FullFilePath))
	{
		// Free Binary Array 	
		ToBinary.FlushCache();
		ToBinary.Empty();

		// UE_LOG(LogSaveGame, Display, TEXT("Save Success!"))
		return true;
	}

	// Free Binary Array 	
	ToBinary.FlushCache();
	ToBinary.Empty();

	// UE_LOG(LogSaveGame, Display, TEXT("File Could Not Be Saved!"))

	return false;
}

bool UBaseSaveGame::LoadGameDataFromFile(const FString& FullFilePath, int32& SaveDataInt32, FVector& SaveDataVector, TArray<FRotator>& SaveDataRotatorArray)
{
	//Load the data array,
	// 	you do not need to pre-initialize this array,
	//		UE4 C++ is awesome and fills it 
	//		with whatever contents of file are, 
	//		and however many bytes that is
	TArray<uint8> TheBinaryArray;
	if (!FFileHelper::LoadFileToArray(TheBinaryArray, *FullFilePath))
	{
		// UE_LOG(LogSaveGame, Display, TEXT("FFILEHELPER:>> Invalid File"))
		return false;
		//~~
	}

	//Testing
	// UE_LOG(LogSaveGame, Display, TEXT("Loaded File Size %s"), *FString::FromInt(TheBinaryArray.Num()))

	//File Load Error
	if (TheBinaryArray.Num() <= 0) return false;

	//~
	//	 Read the Data Retrieved by GFileManager
	//~

	FMemoryReader FromBinary = FMemoryReader(TheBinaryArray, true); //true, free data after done
	FromBinary.Seek(0);
	SaveLoadData(FromBinary, NumGemsCollected, PlayerLocation, ArrayOfRotationsOfTheStars);
	SaveDataInt32 = NumGemsCollected;
	SaveDataVector = PlayerLocation;
	SaveDataRotatorArray = ArrayOfRotationsOfTheStars;

	//~
	//	Clean up 
	//~
	FromBinary.FlushCache();

	// Empty & Close Buffer 
	TheBinaryArray.Empty();
	FromBinary.Close();

	return true;
}

bool UBaseSaveGame::SaveGameDataToFileCompressed(const FString& FullFilePath, int32& SaveDataInt32, FVector& SaveDataVector, TArray<FRotator>& SaveDataRotatorArray)
{
	FBufferArchive ToBinary;
	SaveLoadData(ToBinary, NumGemsCollected, PlayerLocation, ArrayOfRotationsOfTheStars);

	//Pre Compressed Size
	// UE_LOG(LogSaveGame, Display, TEXT("~ PreCompressed Size ~ %s"), *FString::FromInt(ToBinary.Num()))

	//

	// Compress File 
	//tmp compressed data array
	TArray<uint8> CompressedData;

	FArchiveSaveCompressedProxy Compressor = FArchiveSaveCompressedProxy(CompressedData, NAME_Zlib, ECompressionFlags::COMPRESS_ZLIB);

	//Send entire binary array/archive to compressor
	Compressor << ToBinary;

	//send archive serialized data to binary array
	Compressor.Flush();

	//

	//Compressed Size
	// UE_LOG(LogSaveGame, Display, TEXT("~ Compressed Size ~ %s"), *FString::FromInt(CompressedData.Num()))


	// if (!GFileManager) return false;

	//vibes to file, return successful or not
	if (FFileHelper::SaveArrayToFile(CompressedData, *FullFilePath))
	{
		// Free Binary Arrays 
		Compressor.FlushCache();
		CompressedData.Empty();

		ToBinary.FlushCache();
		ToBinary.Empty();

		// Close Buffer 
		ToBinary.Close();

		// UE_LOG(LogSaveGame, Display, TEXT("File Save Success!"))

		return true;
		//
	} else
	{
		// Free Binary Arrays 
		Compressor.FlushCache();
		CompressedData.Empty();

		ToBinary.FlushCache();
		ToBinary.Empty();

		// Close Buffer 
		ToBinary.Close();

		// UE_LOG(LogSaveGame, Display, TEXT("File Could Not Be Saved!"))

		return false;
		//
	}
}

//I am using the sample save data from above examples as the data being loaded
bool UBaseSaveGame::LoadGameDataFromFileCompressed(const FString& FullFilePath, int32& SaveDataInt32, FVector& SaveDataVector, TArray<FRotator>& SaveDataRotatorArray)
{
	//Load the Compressed data array
	TArray<uint8> CompressedData;
	if (!FFileHelper::LoadFileToArray(CompressedData, *FullFilePath))
	{
		// UE_LOG(LogSaveGame, Display, TEXT("FFILEHELPER:>> Invalid File"))
		return false;
		//~~
	}

	// Decompress File 
	FArchiveLoadCompressedProxy Decompressor = FArchiveLoadCompressedProxy(CompressedData, NAME_Zlib, ECompressionFlags::COMPRESS_ZLIB);

	//Decompression Error?
	if (Decompressor.GetError())
	{
		// UE_LOG(LogSaveGame, Display, TEXT("FArchiveLoadCompressedProxy>> ERROR : File Was Not Compressed "))
		return false;
		//
	}

	//Decompress
	FBufferArchive DecompressedBinaryArray;
	Decompressor << DecompressedBinaryArray;

	//~
	//		  Read the Data Retrieved by GFileManager
	//~

	FMemoryReader FromBinary = FMemoryReader(DecompressedBinaryArray, true); //true, free data after done
	FromBinary.Seek(0);
	SaveLoadData(FromBinary, NumGemsCollected, PlayerLocation, ArrayOfRotationsOfTheStars);

	//~
	//								Clean up 
	//~
	CompressedData.Empty();
	Decompressor.FlushCache();
	FromBinary.FlushCache();

	// Empty & Close Buffer 
	DecompressedBinaryArray.Empty();
	DecompressedBinaryArray.Close();

	return true;
}

bool UBaseSaveGame::DoesSaveGameExist(const FString& FullFilePath)
{
	if (IFileManager::Get().FileSize(*FullFilePath) >= 0)
	{
		return true;
	}
	return false;
}

bool UBaseSaveGame::SaveDataToPath(const TArray<uint8>& Data, const FString& FilePath)
{
	return FFileHelper::SaveArrayToFile(Data, *FilePath);
}
